Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | 'use client'; import { useState, useEffect, use } from 'react'; import Link from 'next/link'; import type { MarkdownDocument } from '@/lib/dev-tools/types'; import SafeMarkdown from '@/components/ui/SafeMarkdown'; interface PageProps { params: Promise<{ slug: string }>; } export default function PlanDetailPage({ params }: PageProps) { const { slug } = use(params); const [plan, setPlan] = useState<MarkdownDocument | null>(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { const fetchPlan = async () => { try { const res = await fetch(`/api/admin/dev-tools/docs/plans/${slug}`); if (!res.ok) { if (res.status === 404) throw new Error('Plan not found'); throw new Error('Failed to fetch plan'); } const result = await res.json(); // Handle both new wrapped format and legacy format const data = result.data ?? result; setPlan(data.plan); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load plan'); } finally { setIsLoading(false); } }; fetchPlan(); }, [slug]); if (isLoading) { return ( <div className="max-w-[1170px] mx-auto px-4 sm:px-7.5 xl:px-0"> <div className="animate-pulse"> <div className="h-8 bg-gray-200 rounded w-48 mb-4"></div> <div className="h-4 bg-gray-200 rounded w-full mb-2"></div> <div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div> <div className="h-4 bg-gray-200 rounded w-1/2"></div> </div> </div> ); } if (error || !plan) { return ( <div className="max-w-[1170px] mx-auto px-4 sm:px-7.5 xl:px-0"> <nav className="mb-4"> <ol className="flex items-center gap-2 text-sm text-gray-600"> <li><Link href="/admin/dev-tools" className="hover:text-blue">Developer Tools</Link></li> <li>/</li> <li><Link href="/admin/dev-tools/plans" className="hover:text-blue">Plans</Link></li> </ol> </nav> <div className="bg-red-50 border border-red-200 rounded-lg p-4 text-red-700"> {error || 'Plan not found'} </div> </div> ); } return ( <div className="max-w-[1170px] mx-auto px-4 sm:px-7.5 xl:px-0"> {/* Breadcrumb */} <nav className="mb-4"> <ol className="flex items-center gap-2 text-sm text-gray-600"> <li><Link href="/admin/dev-tools" className="hover:text-blue">Developer Tools</Link></li> <li>/</li> <li><Link href="/admin/dev-tools/plans" className="hover:text-blue">Plans</Link></li> <li>/</li> <li className="text-dark font-medium truncate max-w-[200px]">{plan.title}</li> </ol> </nav> <div className="flex gap-8"> {/* Main Content */} <div className="flex-1 min-w-0"> <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <SafeMarkdown content={plan.content} className="prose prose-sm max-w-none" /> </div> </div> {/* Sidebar - Table of Contents */} {plan.headings.length > 0 && ( <div className="hidden lg:block w-64 flex-shrink-0"> <div className="sticky top-6"> <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4"> <h3 className="font-semibold text-dark mb-3">Table of Contents</h3> <nav className="space-y-1"> {plan.headings.filter(h => h.level <= 3).map((heading, idx) => ( <a key={idx} href={`#${heading.id}`} className={`block text-sm text-gray-600 hover:text-blue transition-colors ${ heading.level === 1 ? 'font-medium' : heading.level === 2 ? 'pl-2' : 'pl-4' }`} > {heading.text} </a> ))} </nav> </div> </div> </div> )} </div> </div> ); } |